

/*-------------------------------------------------------------------------
 *
 * tuplestore.c
 *	  通用的临时元组存储例程。
 *
 * 本模块处理元组的临时存储，目的是用于物化节点、哈希连接批处理文件等。它本质上是tuplesort.c的简化版；它不对元组进行排序，只能存储和再现一系列元组。然而，由于不需要排序，它允许在所有元组写入之前开始读取序列。这对于游标特别有用，因为它允许在已经扫描的查询部分内进行随机访问，而无需将底层扫描处理完毕。此外，支持多个独立的读取指针也是可能的。
 *
 * 如果数据超过调用者指定的空间限制，则使用临时文件来处理数据。
 *
 * 允许给tuplestore的（大致）内存量由调用者以千字节为单位指定。只要没有超过maxKBytes，我们就吸收元组并简单地将其存储在内存数组中。如果超过maxKBytes，我们将所有元组转存到临时文件中，然后在需要时从该文件读取。
 *
 * 在创建时，tuplestore支持一个编号为0的读取指针。可以使用tuplestore_alloc_read_pointer创建额外的读取指针。通过复制读取指针支持标记/恢复行为。
 *
 * 当调用者请求向后扫描能力时，我们将临时文件以允许向前或向后扫描的格式写入。否则，仅允许向前扫描。在将任何元组放入tuplestore之前，必须先发出向后扫描的请求。常规情况下允许回退，但可以通过tuplestore_set_eflags关闭；为所有读取指针关闭回退功能可以在最早的读取点截断tuplestore，以减少内存使用。（调用者必须在适当的时候显式调用tuplestore_trim，以便实际发生截断。）
 *
 * 注意：在TSS_WRITEFILE状态下，临时文件的寻址位置是当前写入位置，tuplestore中的写入位置变量不保持更新。同样，在TSS_READFILE状态下，临时文件的寻址位置是活动读取指针的位置，且该读取指针也不保持更新。我们在切换到其他状态或激活不同的读取指针时使用ftell()更新相关变量。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/utils/sort/tuplestore.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <limits.h>

#include "access/htup_details.h"
#include "commands/tablespace.h"
#include "executor/executor.h"
#include "miscadmin.h"
#include "storage/buffile.h"
#include "utils/memutils.h"
#include "utils/resowner.h"


/*
 * Tuplestore对象的可能状态。这些状态表示在Tuplestore例程调用之间持久存在的状态。
 */
typedef enum
{
	TSS_INMEM,					/* 元组仍然适合内存 */
	TSS_WRITEFILE,				/* 写入临时文件 */
	TSS_READFILE				/* 从临时文件读取 */
} TupStoreStatus;

/*
 * 单个读取指针的状态。如果我们处于INMEM状态，则所有读取指针的“当前”字段表示读取位置。在WRITEFILE状态下，文件/偏移字段表示读取位置。在READFILE状态下，非活动的读取指针具有有效的文件/偏移，但活动的读取指针隐式位置等于临时文件的寻址位置。
 *
 * 特殊情况：如果eof_reached为真，则指针的读取位置隐式等于写入位置，并且current/file/offset不被维护。这样，我们每次写入时都不需要更新所有读取指针。
 */
typedef struct
{
	int			eflags;			/* 能力标志 */
	bool		eof_reached;	/* 读取已达到EOF */
	int			current;		/* 下一个要读取的数组索引 */
	int			file;			/* 临时文件号 */
	off_t		offset;			/* 文件中的字节偏移量 */
} TSReadPointer;

/*
 * Tuplestore操作的私有状态。
 */
struct Tuplestorestate
{
	TupStoreStatus status;		/* 上述显示的枚举值 */
	int			eflags;			/* 能力标志（指针标志的或运算） */
	bool		backward;		/* 是否在文件中存储额外的长度字？ */
	bool		interXact;		/* 在事务中保持打开状态？ */
	bool		truncated;		/* tuplestore_trim是否已移除元组？ */
	int64		availMem;		/* 剩余可用内存，字节数 */
	int64		allowedMem;		/* 允许的总内存，字节数 */
	int64		tuples;			/* 添加的元组数量 */
	BufFile    *myfile;			/* 基础文件，如果没有则为NULL */
	MemoryContext context;		/* 用于保存元组的内存上下文 */
	ResourceOwner resowner;		/* 用于保存临时文件的资源所有者 */

	/*
	 * 这些函数指针将必须知道我们正在处理哪种元组的例程与不需要知道的例程解耦。
	 * 它们由tuplestore_begin_xxx例程设置。
	 *
	 * （虽然tuplestore.c当前只支持堆元组，但我复制了
	 * tuplesort.c的这部分，以便在必要时扩展到其他类型的对象将很容易。）
	 *
	 * 功能是将提供的输入元组复制到palloc的空间中。（注意：我们假设单个pfree()足以稍后释放元组，因此表示必须在一个palloc块中“扁平”。）state->availMem必须减少所使用的空间量。
	 */
	void	   *(*copytup) (Tuplestorestate *state, void *tup);

	/*
	 * 函数将存储的元组写入磁带。磁带上元组的表示不必与内存中的相同；磁带表示的要求如下所述。写入元组后，pfree()它，并增加state->availMem以释放相应的内存空间。
	 */
	void		(*writetup) (Tuplestorestate *state, void *tup);

	/*
	 * 函数从磁带读取存储的元组回到内存中。'len'是已读取的存储元组的长度。创建并返回palloc的副本，并减少state->availMem以消耗的内存空间量。
	 */
	void	   *(*readtup) (Tuplestorestate *state, unsigned int len);

	/*
	 * 这个数组保存指向内存中元组的指针，当我们处于状态 INMEM 时使用。
	 * 在状态 WRITEFILE 和 READFILE 中不使用。
	 *
	 * 当 memtupdeleted > 0 时，前 memtupdeleted 个指针已经
	 * 因为 tuplestore_trim() 操作被释放，但我们尚未花费
	 * 努力来向下移动剩余的指针。 这些未使用的指针
	 * 被设置为 NULL 以捕获任何无效访问。 请注意，memtupcount
	 * 包括被删除的指针。
	 */
	void	  **memtuples;		/* 指向 palloc'd 元组的指针数组 */
	int			memtupdeleted;	/* 前 N 个槽当前未使用 */
	int			memtupcount;	/* 当前存在的元组数量 */
	int			memtupsize;		/* memtuples 数组的分配长度 */
	bool		growmemtuples;	/* memtuples 的增长尚在进行中？ */

	/*
	 * 这些变量用于跟踪当前的位置。
	 *
	 * 在状态 WRITEFILE 中，当前文件的寻址位置是写入点；
	 * 在状态 READFILE 中，写入位置保存在 writepos_xxx 中。
	 * （写入位置与 EOF 相同，但由于 BufFileSeek 不
	 * 目前实现 SEEK_END，因此我们必须显式记住它。）
	 */
	TSReadPointer *readptrs;	/* 读取指针的数组 */
	int			activeptr;		/* 活动读取指针的索引 */
	int			readptrcount;	/* 当前有效的指针数量 */
	int			readptrsize;	/* readptrs 数组的分配长度 */

	int			writepos_file;	/* 文件号（在 READFILE 状态下有效） */
	off_t		writepos_offset;	/* 偏移量（在 READFILE 状态下有效） */
};

#define COPYTUP(state,tup)	((*(state)->copytup) (state, tup))
#define WRITETUP(state,tup) ((*(state)->writetup) (state, tup))
#define READTUP(state,len)	((*(state)->readtup) (state, len))
#define LACKMEM(state)		((state)->availMem < 0)
#define USEMEM(state,amt)	((state)->availMem -= (amt))
#define FREEMEM(state,amt)	((state)->availMem += (amt))

/*--------------------
 *
 * 有关元组在磁带表示的说明：
 *
 * 我们要求存储元组的第一个 "unsigned int" 为元组在磁带上的总大小，
 * 包括自身（因此它永远不会为零）。
 * 存储元组的其余部分
 * 可能与内存中的元组表示相匹配，也可能不匹配---
 * 所需的任何转换都是 writetup 和 readtup 例程的工作。
 *
 * 如果状态的 backward 为 true，则存储的元组表示
 * 后面必须跟随另一个 "unsigned int"，该值是长度的副本---
 * 因此使用的总磁带空间实际上比存储的长度值多了 sizeof(unsigned int)
 * 。这允许向后读取。当状态的 backward 未设置时，写入/读取例程可以省略额外的
 * 长度字。
 *
 * writetup 预计将写入两个长度字以及元组
 * 数据。当调用 readtup 时，磁带的位置正好在
 * 前长度字之后； readtup 必须读取元组数据并向前推进
 * 经过后长度字（如果存在）。
 *
 * 写入/读取例程可以根据需要利用存储在 Tuplestorestate 记录中的元组描述数据。
 * 他们还预计根据释放或消耗的内存空间（而不是磁带空间！）
 * 调整状态的 availMem。writetup 和 readtup 都没有错误返回；
 * 它们在失败时应调用 ereport()。
 *
 *
 * 有关内存消耗计算的说明：
 *
 * 我们将分配给元组的空间计入 maxKBytes 限制，
 * 加上变量大小数组 memtuples 使用的空间。
 * 固定大小的空间（主要是 BufFile I/O 缓冲区）不计入。
 * 我们也不担心读取指针数组的大小。
 *
 * 请注意，我们计算的是实际使用的空间（如 GetMemoryChunkSpace 所示）
 * 而不是最初请求的大小。这一点非常重要，因为
 * palloc 可能会增加相当大的开销。这并不是一个完整的答案，因为
 * 我们不会计算 palloc 分配块中的任何浪费空间，但这
 * 比我们在 7.3 之前所做的要好得多。
 *
 *--------------------
 */


static Tuplestorestate *fc_tuplestore_begin_common(int fc_eflags,
												bool fc_interXact,
												int fc_maxKBytes);
static void fc_tuplestore_puttuple_common(Tuplestorestate *fc_state, void *fc_tuple);
static void fc_dumptuples(Tuplestorestate *fc_state);
static unsigned int fc_getlen(Tuplestorestate *fc_state, bool fc_eofOK);
static void *fc_copytup_heap(Tuplestorestate *fc_state, void *fc_tup);
static void fc_writetup_heap(Tuplestorestate *fc_state, void *fc_tup);
static void *fc_readtup_heap(Tuplestorestate *fc_state, unsigned int fc_len);


/*
 *		tuplestore_begin_xxx
 *
 * 初始化元组存储操作。
 */
static Tuplestorestate * fc_tuplestore_begin_common(int fc_eflags, bool fc_interXact, int fc_maxKBytes)
{
	Tuplestorestate *fc_state;

	fc_state = (Tuplestorestate *) palloc0(sizeof(Tuplestorestate));

	fc_state->status = TSS_INMEM;
	fc_state->eflags = fc_eflags;
	fc_state->interXact = fc_interXact;
	fc_state->truncated = false;
	fc_state->allowedMem = fc_maxKBytes * 1024L;
	fc_state->availMem = fc_state->allowedMem;
	fc_state->myfile = NULL;
	fc_state->context = CurrentMemoryContext;
	fc_state->resowner = CurrentResourceOwner;

	fc_state->memtupdeleted = 0;
	fc_state->memtupcount = 0;
	fc_state->tuples = 0;

	/*
	 * 数组的初始大小必须超过 ALLOCSET_SEPARATE_THRESHOLD；
	 * 请参见 grow_memtuples() 中的注释。
	 */
	fc_state->memtupsize = Max(16384 / sizeof(void *),
							ALLOCSET_SEPARATE_THRESHOLD / sizeof(void *) + 1);

	fc_state->growmemtuples = true;
	fc_state->memtuples = (void **) palloc(fc_state->memtupsize * sizeof(void *));

	USEMEM(fc_state, GetMemoryChunkSpace(fc_state->memtuples));

	fc_state->activeptr = 0;
	fc_state->readptrcount = 1;
	fc_state->readptrsize = 8;		/* 随意 */
	fc_state->readptrs = (TSReadPointer *)
		palloc(fc_state->readptrsize * sizeof(TSReadPointer));

	fc_state->readptrs[0].eflags = fc_eflags;
	fc_state->readptrs[0].eof_reached = false;
	fc_state->readptrs[0].current = 0;

	return fc_state;
}

/*
 * tuplestore_begin_heap
 *
 * 创建一个新的 tuplestore；其他类型的元组存储（除了
 * “heap” 元组存储，用于 heap 元组）是可能的，但目前
 * 未实现。
 *
 * randomAccess：如果为真，则允许对
 * 元组存储进行向前和向后的访问。
 *
 * interXact：如果为真，则用于磁盘存储的文件在
 * 当前事务结束后仍然存在。注意：创建这样一个 tuplestore 
 * 在一个内存上下文和资源所有者中是调用者的责任，以便它也能
 * 超越事务边界，并确保在不再需要时关闭 tuplestore。
 *
 * maxKBytes：在内存中存储多少数据（超过此
 * 数量的数据将分页到磁盘）。如果不确定，请使用 work_mem。
 */
Tuplestorestate *
tuplestore_begin_heap(bool fc_randomAccess, bool fc_interXact, int fc_maxKBytes)
{
	Tuplestorestate *fc_state;
	int			fc_eflags;

	/*
	 * 这种对 randomAccess 意义的解释与
	 * tuplestores 的 8.3 之前的行为兼容。
	 */
	fc_eflags = fc_randomAccess ?
		(EXEC_FLAG_BACKWARD | EXEC_FLAG_REWIND) :
		(EXEC_FLAG_REWIND);

	fc_state = fc_tuplestore_begin_common(fc_eflags, fc_interXact, fc_maxKBytes);

	fc_state->copytup = fc_copytup_heap;
	fc_state->writetup = fc_writetup_heap;
	fc_state->readtup = fc_readtup_heap;

	return fc_state;
}

/*
 * tuplestore_set_eflags
 *
 * 设置比 tuplestore_begin_xxx 允许的更细粒度的读指针 0 的能力标志。
 * 必须在向 tuplestore 插入任何数据之前调用此函数。
 *
 * eflags 是一个位掩码，遵循 executor 节点
 * 启动标志的含义（见 executor.h）。tuplestore 会关注以下位：
 *		EXEC_FLAG_REWIND		需要重绕到开始
 *		EXEC_FLAG_BACKWARD		需要向后获取
 * 如果未调用 tuplestore_set_eflags，则允许 REWIND，并且 BACKWARD
 * 根据 tuplestore_begin_xxx 调用中的 “randomAccess” 设置。
 *
 * 注意：在没有 REWIND 的情况下设置 BACKWARD 意味着指针可以向后读取，
 * 但不能超过截断点（在上次 tuplestore_trim 调用时的
 * 最长向后读取指针位置）。
 */
void tuplestore_set_eflags(Tuplestorestate *fc_state, int fc_eflags)
{
	int			fc_i;

	if (fc_state->status != TSS_INMEM || fc_state->memtupcount != 0)
		elog(ERROR, "too late to call tuplestore_set_eflags");

	fc_state->readptrs[0].eflags = fc_eflags;
	for (fc_i = 1; fc_i < fc_state->readptrcount; fc_i++)
		fc_eflags |= fc_state->readptrs[fc_i].eflags;
	fc_state->eflags = fc_eflags;
}

/*
 * tuplestore_alloc_read_pointer - 分配另一个读取指针。
 *
 * 返回指针的索引。
 *
 * 新指针最初复制读取指针 0 的位置。
 * 它可以有自己的 eflags，但如果已向
 * tuplestore 中插入任何数据，这些 eflags 不能代表需求的增加。
 */
int tuplestore_alloc_read_pointer(Tuplestorestate *fc_state, int fc_eflags)
{
	/* 检查需求的可能增加 */
	if (fc_state->status != TSS_INMEM || fc_state->memtupcount != 0)
	{
		if ((fc_state->eflags | fc_eflags) != fc_state->eflags)
			elog(ERROR, "too late to require new tuplestore eflags");
	}

	/* 如有需要，腾出空间以便再增加一个读取指针 */
	if (fc_state->readptrcount >= fc_state->readptrsize)
	{
		int			fc_newcnt = fc_state->readptrsize * 2;

		fc_state->readptrs = (TSReadPointer *)
			repalloc(fc_state->readptrs, fc_newcnt * sizeof(TSReadPointer));
		fc_state->readptrsize = fc_newcnt;
	}

	/* 并进行设置 */
	fc_state->readptrs[fc_state->readptrcount] = fc_state->readptrs[0];
	fc_state->readptrs[fc_state->readptrcount].eflags = fc_eflags;

	fc_state->eflags |= fc_eflags;

	return fc_state->readptrcount++;
}

/*
 * tuplestore_clear
 *
 * 删除 tuplestore 的所有内容，并将其读取指针
 * 重置到开始位置。
 */
void tuplestore_clear(Tuplestorestate *fc_state)
{
	int			fc_i;
	TSReadPointer *fc_readptr;

	if (fc_state->myfile)
		BufFileClose(fc_state->myfile);
	fc_state->myfile = NULL;
	if (fc_state->memtuples)
	{
		for (fc_i = fc_state->memtupdeleted; fc_i < fc_state->memtupcount; fc_i++)
		{
			FREEMEM(fc_state, GetMemoryChunkSpace(fc_state->memtuples[fc_i]));
			pfree(fc_state->memtuples[fc_i]);
		}
	}
	fc_state->status = TSS_INMEM;
	fc_state->truncated = false;
	fc_state->memtupdeleted = 0;
	fc_state->memtupcount = 0;
	fc_state->tuples = 0;
	fc_readptr = fc_state->readptrs;
	for (fc_i = 0; fc_i < fc_state->readptrcount; fc_readptr++, fc_i++)
	{
		fc_readptr->eof_reached = false;
		fc_readptr->current = 0;
	}
}

/*
 * tuplestore_end
 *
 * 释放资源并进行清理。
 */
void tuplestore_end(Tuplestorestate *fc_state)
{
	int			fc_i;

	if (fc_state->myfile)
		BufFileClose(fc_state->myfile);
	if (fc_state->memtuples)
	{
		for (fc_i = fc_state->memtupdeleted; fc_i < fc_state->memtupcount; fc_i++)
			pfree(fc_state->memtuples[fc_i]);
		pfree(fc_state->memtuples);
	}
	pfree(fc_state->readptrs);
	pfree(fc_state);
}

/*
 * tuplestore_select_read_pointer - 使指定的读取指针处于活动状态
 */
void tuplestore_select_read_pointer(Tuplestorestate *fc_state, int fc_ptr)
{
	TSReadPointer *fc_readptr;
	TSReadPointer *fc_oldptr;

	Assert(fc_ptr >= 0 && fc_ptr < fc_state->readptrcount);

	/* 如果已经是活动状态则不需要工作 */
	if (fc_ptr == fc_state->activeptr)
		return;

	fc_readptr = &fc_state->readptrs[fc_ptr];
	fc_oldptr = &fc_state->readptrs[fc_state->activeptr];

	switch (fc_state->status)
	{
		case TSS_INMEM:
		case TSS_WRITEFILE:
			/* 无工作 */
			break;
		case TSS_READFILE:

			/*
			 * 首先，保存即将变为非活动状态的指针中的当前读取位置。
			 */
			if (!fc_oldptr->eof_reached)
				BufFileTell(fc_state->myfile,
							&fc_oldptr->file,
							&fc_oldptr->offset);

			/*
			 * 我们必须使临时文件的寻道位置等于
			 * 新读取指针的逻辑位置。在 eof_reached
			 * 状态下，那就是 EOF，我们可以从保存的
			 * 写入位置中获得。
			 */
			if (fc_readptr->eof_reached)
			{
				if (BufFileSeek(fc_state->myfile,
								fc_state->writepos_file,
								fc_state->writepos_offset,
								SEEK_SET) != 0)
					ereport(ERROR,
							(errcode_for_file_access(),
							 errmsg("could not seek in tuplestore temporary file")));
			}
			else
			{
				if (BufFileSeek(fc_state->myfile,
								fc_readptr->file,
								fc_readptr->offset,
								SEEK_SET) != 0)
					ereport(ERROR,
							(errcode_for_file_access(),
							 errmsg("could not seek in tuplestore temporary file")));
			}
			break;
		default:
			elog(ERROR, "invalid tuplestore state");
			break;
	}

	fc_state->activeptr = fc_ptr;
}

/*
 * tuplestore_tuple_count
 *
 * 返回自创建或上次 tuplestore_clear() 以来添加的元组数量。
 */
int64 tuplestore_tuple_count(Tuplestorestate *fc_state)
{
	return fc_state->tuples;
}

/*
 * tuplestore_ateof
 *
 * 返回活动读取指针的 eof_reached 状态。
 */
bool tuplestore_ateof(Tuplestorestate *fc_state)
{
	return fc_state->readptrs[fc_state->activeptr].eof_reached;
}

/*
 * 如果可能的话，扩大 memtuples[] 数组，同时遵守我们的内存限制。
 * 我们不能超过内存中的 INT_MAX 个元组或调用者提供的内存限制。
 * 如果我们能够扩大数组，返回 true；否则返回 false。
 *
 * 通常，在每次增量时我们会将数组的大小加倍。当这样做会超过限制时，
 * 我们尝试最后一次较小的增加（然后清除 growmemtuples 标志，以便我们
 * 不再尝试）。这使我们能够充分利用内存；坚持纯粹的加倍规则可能会
 * 导致几乎一半的内存未被使用。由于 availMem 随着元组的增加/删除而
 * 变化，我们需要某种规则以防止在 memtupsize 中进行重复的小增加，
 * 这将无益于处理。growmemtuples 标志实现了这一点，并且还防止了该
 * 函数中的无用重计算。
 */
static bool fc_grow_memtuples(Tuplestorestate *fc_state)
{
	int			fc_newmemtupsize;
	int			fc_memtupsize = fc_state->memtupsize;
	int64		fc_memNowUsed = fc_state->allowedMem - fc_state->availMem;

	/* 如果我们已经用尽了 memtuples，请忘掉它，如上面的评论所述 */
	if (!fc_state->growmemtuples)
		return false;

	/* 选择 memtupsize 的新值 */
	if (fc_memNowUsed <= fc_state->availMem)
	{
		/*
		 * 我们使用的内存没有超过 allowedMem 的一半；将我们的使用量
		 * 翻倍，限制为 INT_MAX 个元组。
		 */
		if (fc_memtupsize < INT_MAX / 2)
			fc_newmemtupsize = fc_memtupsize * 2;
		else
		{
			fc_newmemtupsize = INT_MAX;
			fc_state->growmemtuples = false;
		}
	}
	else
	{
		/*
		 * 这将是 memtupsize 的最后一次增量。放弃加倍策略，
		 * 而是尽可能安全地增加。
		 *
		 * 为了保持在 allowedMem 内，我们不能将 memtupsize 增加超过
		 * availMem / sizeof(void *) 的元素数。实际上，我们希望
		 * 将其增加得少得多，因为我们需要为新数组槽所引用的元组留出一些
		 * 空间。我们假设新元组的大小大约与我们已经看到的元组相同，
		 * 因此我们可以根据迄今为止的空间消耗来推断一个合适的
		 * memtuples 数组的新大小。最佳值可能高于或低于
		 * 这一估计，但事先很难知道。我们再次限制在 INT_MAX 元组数。
		 *
		 * 此计算在扩展数组时是安全的，不会导致 LACKMEM 为真，
		 * 因为当前使用的内存包括当前数组；因此，即使当前没有使用其他内存，
		 * 也有足够的 allowedMem 用于新数组元素。
		 *
		 * 我们在 float8 中进行算术计算，因为否则 memtupsize 和
		 * allowedMem 的乘积可能会溢出。结果中的任何不准确都应该是微不足道的；
		 * 但即使我们计算出一个完全疯狂的结果，下面的检查也会防止发生任何
		 * 真正糟糕的事情。
		 */
		double		fc_grow_ratio;

		fc_grow_ratio = (double) fc_state->allowedMem / (double) fc_memNowUsed;
		if (fc_memtupsize * fc_grow_ratio < INT_MAX)
			fc_newmemtupsize = (int) (fc_memtupsize * fc_grow_ratio);
		else
			fc_newmemtupsize = INT_MAX;

		/* 我们不会进行任何进一步的扩大尝试 */
		fc_state->growmemtuples = false;
	}

	/* 必须至少扩大数组一个元素，否则报告失败 */
	if (fc_newmemtupsize <= fc_memtupsize)
		goto noalloc;

	/*
	 * 在 32 位机器上，allowedMem 可能超过 MaxAllocHugeSize。
	 * 限制以确保我们的请求不会被拒绝。请注意，我们可以很容易地
	 * 耗尽地址空间而不会面临这种结果。（目前由于 guc.c 的
	 * MAX_KILOBYTES 对 work_mem 的限制，这种情况是不可能的，
	 * 但在这个距离上不必依赖于此。）
	 */
	if ((Size) fc_newmemtupsize >= MaxAllocHugeSize / sizeof(void *))
	{
		fc_newmemtupsize = (int) (MaxAllocHugeSize / sizeof(void *));
		fc_state->growmemtuples = false;	/* 不能再增长了 */
	}

	/*
	 * 我们需要确保不造成 LACKMEM 为真，否则空间管理算法会失控。
	 * 上面的代码绝对不应该生成危险的请求，但为了安全起见，
	 * 明确检查数组增长是否在 availMem 范围内。 (如果与 memtuples
	 * 数组相关的内存块开销增加，我们仍然可能会导致 LACKMEM。
	 * 这不应该发生，因为我们选择的初始数组大小足够大，以确保
	 * palloc 会将旧数组和新数组视为独立块。但为了安全起见，
	 * 我们将在下面明确检查 LACKMEM。)
	 */
	if (fc_state->availMem < (int64) ((fc_newmemtupsize - fc_memtupsize) * sizeof(void *)))
		goto noalloc;

	/* 注意：我们不在表达式树上标记排序规则 */
	FREEMEM(fc_state, GetMemoryChunkSpace(fc_state->memtuples));
	fc_state->memtupsize = fc_newmemtupsize;
	fc_state->memtuples = (void **)
		repalloc_huge(fc_state->memtuples,
					  fc_state->memtupsize * sizeof(void *));
	USEMEM(fc_state, GetMemoryChunkSpace(fc_state->memtuples));
	if (LACKMEM(fc_state))
		elog(ERROR, "unexpected out-of-memory situation in tuplestore");
	return true;

noalloc:
	/* 如果出于任何原因我们没有重新分配，关闭未来的尝试 */
	fc_state->growmemtuples = false;
	return false;
}

/*
 * 接受一个元组并将其附加到元组存储。
 *
 * 请注意，输入元组始终被复制；调用者无须保存它。
 *
 * 如果活动读取指针当前“在EOF处”，则保持不变（读取指针隐式与写入指针一起推进）；否则，读取指针不变。非活动读取指针不会移动，这意味着它们在puttuple之后一定不会“在EOF处”。这种看似奇怪的行为是为了方便nodeMaterial.c和nodeCtescan.c，否则它们需要进行额外的指针重新定位步骤。
 *
 * tuplestore_puttupleslot()是一个方便的例程，可以从TupleTableSlot收集数据，而无需额外的复制操作。
 */
void tuplestore_puttupleslot(Tuplestorestate *fc_state,
						TupleTableSlot *fc_slot)
{
	MinimalTuple fc_tuple;
	MemoryContext fc_oldcxt = MemoryContextSwitchTo(fc_state->context);

	/*
	 * 在工作内存中形成MinimalTuple
	 */
	fc_tuple = ExecCopySlotMinimalTuple(fc_slot);
	USEMEM(fc_state, GetMemoryChunkSpace(fc_tuple));

	fc_tuplestore_puttuple_common(fc_state, (void *) fc_tuple);

	MemoryContextSwitchTo(fc_oldcxt);
}

/*
 * 从HeapTuple复制的“标准”情况。实际上现在有些不推荐使用，但考虑到呼叫者的数量，不值得去掉。
 */
void tuplestore_puttuple(Tuplestorestate *fc_state, HeapTuple fc_tuple)
{
	MemoryContext fc_oldcxt = MemoryContextSwitchTo(fc_state->context);

	/*
	 * 复制元组。（即使在WRITEFILE情况下也必须这样做。请注意COPYTUP包括USEMEM，因此我们在这里不需要这样做。）
	 */
	fc_tuple = COPYTUP(fc_state, fc_tuple);

	fc_tuplestore_puttuple_common(fc_state, (void *) fc_tuple);

	MemoryContextSwitchTo(fc_oldcxt);
}

/*
 * 类似于tuplestore_puttuple()，但从值和空值数组中工作。这避免了额外的元组构造操作。
 */
void tuplestore_putvalues(Tuplestorestate *fc_state, TupleDesc fc_tdesc,
					 Datum *fc_values, bool *fc_isnull)
{
	MinimalTuple fc_tuple;
	MemoryContext fc_oldcxt = MemoryContextSwitchTo(fc_state->context);

	fc_tuple = heap_form_minimal_tuple(fc_tdesc, fc_values, fc_isnull);
	USEMEM(fc_state, GetMemoryChunkSpace(fc_tuple));

	fc_tuplestore_puttuple_common(fc_state, (void *) fc_tuple);

	MemoryContextSwitchTo(fc_oldcxt);
}

static void fc_tuplestore_puttuple_common(Tuplestorestate *fc_state, void *fc_tuple)
{
	TSReadPointer *fc_readptr;
	int			fc_i;
	ResourceOwner fc_oldowner;

	fc_state->tuples++;

	switch (fc_state->status)
	{
		case TSS_INMEM:

			/*
			 * 根据需要更新读取指针；参见上面的API规范。
			 */
			fc_readptr = fc_state->readptrs;
			for (fc_i = 0; fc_i < fc_state->readptrcount; fc_readptr++, fc_i++)
			{
				if (fc_readptr->eof_reached && fc_i != fc_state->activeptr)
				{
					fc_readptr->eof_reached = false;
					fc_readptr->current = fc_state->memtupcount;
				}
			}

			/*
			 * 根据需要扩展数组。请注意，当仍然有一个空槽时，我们会尝试扩展数组——如果失败，仍然有空间存储传入的元组，然后我们将切换到基于磁带的操作。
			 */
			if (fc_state->memtupcount >= fc_state->memtupsize - 1)
			{
				(void) fc_grow_memtuples(fc_state);
				Assert(fc_state->memtupcount < fc_state->memtupsize);
			}

			/* 将元组存放在内存数组中 */
			fc_state->memtuples[fc_state->memtupcount++] = fc_tuple;

			/*
			 * 如果我们仍然适合可用内存并且有数组槽，完成。
			 */
			if (fc_state->memtupcount < fc_state->memtupsize && !LACKMEM(fc_state))
				return;

			/*
			 * 不行；是时候切换到基于磁带的操作了。确保在合适的临时表空间中创建临时文件。
			 */
			PrepareTempTablespaces();

			/* 将文件与商店的资源所有者关联 */
			fc_oldowner = CurrentResourceOwner;
			CurrentResourceOwner = fc_state->resowner;

			fc_state->myfile = BufFileCreateTemp(fc_state->interXact);

			CurrentResourceOwner = fc_oldowner;

			/*
			 * 冻结关于是否使用尾部长度词的决定。
			 * 一旦数据在磁带上，我们无法更改这个选择，即使调用者可能放弃这个要求。
			 */
			fc_state->backward = (fc_state->eflags & EXEC_FLAG_BACKWARD) != 0;
			fc_state->status = TSS_WRITEFILE;
			fc_dumptuples(fc_state);
			break;
		case TSS_WRITEFILE:

			/*
			 * 根据需要更新读取指针；详见上面的API规范。注意：
			 * BufFileTell是相当便宜的，因此不值得尝试避免
			 * 多次调用。
			 */
			fc_readptr = fc_state->readptrs;
			for (fc_i = 0; fc_i < fc_state->readptrcount; fc_readptr++, fc_i++)
			{
				if (fc_readptr->eof_reached && fc_i != fc_state->activeptr)
				{
					fc_readptr->eof_reached = false;
					BufFileTell(fc_state->myfile,
								&fc_readptr->file,
								&fc_readptr->offset);
				}
			}

			WRITETUP(fc_state, fc_tuple);
			break;
		case TSS_READFILE:

			/*
			 * 从读取切换到写入。
			 */
			if (!fc_state->readptrs[fc_state->activeptr].eof_reached)
				BufFileTell(fc_state->myfile,
							&fc_state->readptrs[fc_state->activeptr].file,
							&fc_state->readptrs[fc_state->activeptr].offset);
			if (BufFileSeek(fc_state->myfile,
							fc_state->writepos_file, fc_state->writepos_offset,
							SEEK_SET) != 0)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not seek in tuplestore temporary file")));
			fc_state->status = TSS_WRITEFILE;

			/*
			 * 根据需要更新读取指针；参见上面的API规范。
			 */
			fc_readptr = fc_state->readptrs;
			for (fc_i = 0; fc_i < fc_state->readptrcount; fc_readptr++, fc_i++)
			{
				if (fc_readptr->eof_reached && fc_i != fc_state->activeptr)
				{
					fc_readptr->eof_reached = false;
					fc_readptr->file = fc_state->writepos_file;
					fc_readptr->offset = fc_state->writepos_offset;
				}
			}

			WRITETUP(fc_state, fc_tuple);
			break;
		default:
			elog(ERROR, "invalid tuplestore state");
			break;
	}
}

/*
 * 在前向或后向方向上获取下一个元组。
 * 如果没有更多元组则返回NULL。如果should_free被设置，调用者必须在使用完返回的元组后进行pfree。
 *
 * 仅当randomAccess被设置为true或指定EXEC_FLAG_BACKWARD给tuplestore_set_eflags()时，才允许向后扫描。
 */
static void * fc_tuplestore_gettuple(Tuplestorestate *fc_state, bool fc_forward,
					bool *fc_should_free)
{
	TSReadPointer *fc_readptr = &fc_state->readptrs[fc_state->activeptr];
	unsigned int fc_tuplen;
	void	   *fc_tup;

	Assert(fc_forward || (fc_readptr->eflags & EXEC_FLAG_BACKWARD));

	switch (fc_state->status)
	{
		case TSS_INMEM:
			*fc_should_free = false;
			if (fc_forward)
			{
				if (fc_readptr->eof_reached)
					return NULL;
				if (fc_readptr->current < fc_state->memtupcount)
				{
					/* 我们有另一个元组，所以返回它 */
					return fc_state->memtuples[fc_readptr->current++];
				}
				fc_readptr->eof_reached = true;
				return NULL;
			}
			else
			{
				/*
				 * 如果所有元组已经被提取，则返回最后一个
				 * 元组，否则返回倒数第二个元组。
				 */
				if (fc_readptr->eof_reached)
				{
					fc_readptr->current = fc_state->memtupcount;
					fc_readptr->eof_reached = false;
				}
				else
				{
					if (fc_readptr->current <= fc_state->memtupdeleted)
					{
						Assert(!fc_state->truncated);
						return NULL;
					}
					fc_readptr->current--; /* 最近返回的元组 */
				}
				if (fc_readptr->current <= fc_state->memtupdeleted)
				{
					Assert(!fc_state->truncated);
					return NULL;
				}
				return fc_state->memtuples[fc_readptr->current - 1];
			}
			break;

		case TSS_WRITEFILE:
			/* 如果我们只是返回NULL，则跳过状态更改 */
			if (fc_readptr->eof_reached && fc_forward)
				return NULL;

			/*
			 * 从写入切换到读取。
			 */
			BufFileTell(fc_state->myfile,
						&fc_state->writepos_file, &fc_state->writepos_offset);
			if (!fc_readptr->eof_reached)
				if (BufFileSeek(fc_state->myfile,
								fc_readptr->file, fc_readptr->offset,
								SEEK_SET) != 0)
					ereport(ERROR,
							(errcode_for_file_access(),
							 errmsg("could not seek in tuplestore temporary file")));
			fc_state->status = TSS_READFILE;
			/* FALLTHROUGH */

		case TSS_READFILE:
			*fc_should_free = true;
			if (fc_forward)
			{
				if ((fc_tuplen = fc_getlen(fc_state, true)) != 0)
				{
					fc_tup = READTUP(fc_state, fc_tuplen);
					return fc_tup;
				}
				else
				{
					fc_readptr->eof_reached = true;
					return NULL;
				}
			}

			/*
			 * 向后。
			 *
			 * 如果所有元组已经被提取，则返回最后一个元组，
			 * 否则返回倒数第二个元组。
			 *
			 * 向后以获取之前返回的元组的结束长度
			 * 词。如果查找失败，假设我们在文件的开头。
			 */
			if (BufFileSeek(fc_state->myfile, 0, -(long) sizeof(unsigned int),
							SEEK_CUR) != 0)
			{
				/* 即使向后提取失败也会让你脱离eof状态 */
				fc_readptr->eof_reached = false;
				Assert(!fc_state->truncated);
				return NULL;
			}
			fc_tuplen = fc_getlen(fc_state, false);

			if (fc_readptr->eof_reached)
			{
				fc_readptr->eof_reached = false;
				/* 我们将返回在返回NULL之前返回的元组 */
			}
			else
			{
				/*
				 * 向后获取其前面的元组的结束长度单词。
				 */
				if (BufFileSeek(fc_state->myfile, 0,
								-(long) (fc_tuplen + 2 * sizeof(unsigned int)),
								SEEK_CUR) != 0)
				{
					/*
					 * 如果这失败，推测前一个元组是文件中的第一个。
					 * 向后移动，以便它成为下一个要向前读取的元组
					 * （这显然不完全正确，但这是内存中的情况）。
					 */
					if (BufFileSeek(fc_state->myfile, 0,
									-(long) (fc_tuplen + sizeof(unsigned int)),
									SEEK_CUR) != 0)
						ereport(ERROR,
								(errcode_for_file_access(),
								 errmsg("could not seek in tuplestore temporary file")));
					Assert(!fc_state->truncated);
					return NULL;
				}
				fc_tuplen = fc_getlen(fc_state, false);
			}

			/*
			 * 现在我们有了前一个元组的长度，回退并读取它。
			 * 注意：READTUP 期望我们位于元组的初始长度词之后，所以回退到那个点。
			 */
			if (BufFileSeek(fc_state->myfile, 0,
							-(long) fc_tuplen,
							SEEK_CUR) != 0)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not seek in tuplestore temporary file")));
			fc_tup = READTUP(fc_state, fc_tuplen);
			return fc_tup;

		default:
			elog(ERROR, "invalid tuplestore state");
			return NULL;		/* 保持编译器安静 */
	}
}

/*
 * tuplestore_gettupleslot - 导出函数以获取MinimalTuple
 *
 * 如果成功，则将元组放入槽中并返回true；否则，清除槽并返回false。
 *
 * 如果copy为true，则槽接收一个复制的元组（分配在当前
 * 内存上下文中），该元组在将来对
 * tuplestore状态的操作中将保持有效。如果copy为false，则槽可能仅接收
 * 一个指向保存在tuplestore中的元组的指针。后者效率更高，但如果
 * 对tuplestore进行额外写入，则槽的内容可能会损坏。 （如果使用tuplestore_trim，请参见其中的注释。）
 */
bool tuplestore_gettupleslot(Tuplestorestate *fc_state, bool fc_forward,
						bool fc_copy, TupleTableSlot *fc_slot)
{
	MinimalTuple fc_tuple;
	bool		fc_should_free;

	fc_tuple = (MinimalTuple) fc_tuplestore_gettuple(fc_state, fc_forward, &fc_should_free);

	if (fc_tuple)
	{
		if (fc_copy && !fc_should_free)
		{
			fc_tuple = heap_copy_minimal_tuple(fc_tuple);
			fc_should_free = true;
		}
		ExecStoreMinimalTuple(fc_tuple, fc_slot, fc_should_free);
		return true;
	}
	else
	{
		ExecClearTuple(fc_slot);
		return false;
	}
}

/*
 * tuplestore_advance - 导出函数以在不获取的情况下调整位置
 *
 * 我们可以优化这种情况以避免palloc/pfree开销，但目前似乎没有必要。
 */
bool tuplestore_advance(Tuplestorestate *fc_state, bool fc_forward)
{
	void	   *fc_tuple;
	bool		fc_should_free;

	fc_tuple = fc_tuplestore_gettuple(fc_state, fc_forward, &fc_should_free);

	if (fc_tuple)
	{
		if (fc_should_free)
			pfree(fc_tuple);
		return true;
	}
	else
	{
		return false;
	}
}

/*
 * 在 N 个元组之间向前或向后推进，
 * 不返回任何数据。 N<=0 是不操作。
 * 如果成功返回 true，如果没有元组则返回 false。
 */
bool tuplestore_skiptuples(Tuplestorestate *fc_state, int64 fc_ntuples, bool fc_forward)
{
	TSReadPointer *fc_readptr = &fc_state->readptrs[fc_state->activeptr];

	Assert(fc_forward || (fc_readptr->eflags & EXEC_FLAG_BACKWARD));

	if (fc_ntuples <= 0)
		return true;

	switch (fc_state->status)
	{
		case TSS_INMEM:
			if (fc_forward)
			{
				if (fc_readptr->eof_reached)
					return false;
				if (fc_state->memtupcount - fc_readptr->current >= fc_ntuples)
				{
					fc_readptr->current += fc_ntuples;
					return true;
				}
				fc_readptr->current = fc_state->memtupcount;
				fc_readptr->eof_reached = true;
				return false;
			}
			else
			{
				if (fc_readptr->eof_reached)
				{
					fc_readptr->current = fc_state->memtupcount;
					fc_readptr->eof_reached = false;
					fc_ntuples--;
				}
				if (fc_readptr->current - fc_state->memtupdeleted > fc_ntuples)
				{
					fc_readptr->current -= fc_ntuples;
					return true;
				}
				Assert(!fc_state->truncated);
				fc_readptr->current = fc_state->memtupdeleted;
				return false;
			}
			break;

		default:
			/* 我们目前不努力优化其他情况 */
			while (fc_ntuples-- > 0)
			{
				void	   *fc_tuple;
				bool		fc_should_free;

				fc_tuple = fc_tuplestore_gettuple(fc_state, fc_forward, &fc_should_free);

				if (fc_tuple == NULL)
					return false;
				if (fc_should_free)
					pfree(fc_tuple);
				CHECK_FOR_INTERRUPTS();
			}
			return true;
	}
}

/*
 * dumptuples - 从内存中移除元组并写入磁带
 *
 * 作为副作用，我们必须将每个读指针的位置从
 * “当前”转换为文件/偏移格式。但 eof_reached 指针不
 * 需要更改状态。
 */
static void fc_dumptuples(Tuplestorestate *fc_state)
{
	int			fc_i;

	for (fc_i = fc_state->memtupdeleted;; fc_i++)
	{
		TSReadPointer *fc_readptr = fc_state->readptrs;
		int			fc_j;

		for (fc_j = 0; fc_j < fc_state->readptrcount; fc_readptr++, fc_j++)
		{
			if (fc_i == fc_readptr->current && !fc_readptr->eof_reached)
				BufFileTell(fc_state->myfile,
							&fc_readptr->file, &fc_readptr->offset);
		}
		if (fc_i >= fc_state->memtupcount)
			break;
		WRITETUP(fc_state, fc_state->memtuples[fc_i]);
	}
	fc_state->memtupdeleted = 0;
	fc_state->memtupcount = 0;
}

/*
 * tuplestore_rescan - 将活动读指针重绕到开头
 */
void tuplestore_rescan(Tuplestorestate *fc_state)
{
	TSReadPointer *fc_readptr = &fc_state->readptrs[fc_state->activeptr];

	Assert(fc_readptr->eflags & EXEC_FLAG_REWIND);
	Assert(!fc_state->truncated);

	switch (fc_state->status)
	{
		case TSS_INMEM:
			fc_readptr->eof_reached = false;
			fc_readptr->current = 0;
			break;
		case TSS_WRITEFILE:
			fc_readptr->eof_reached = false;
			fc_readptr->file = 0;
			fc_readptr->offset = 0L;
			break;
		case TSS_READFILE:
			fc_readptr->eof_reached = false;
			if (BufFileSeek(fc_state->myfile, 0, 0L, SEEK_SET) != 0)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not seek in tuplestore temporary file")));
			break;
		default:
			elog(ERROR, "invalid tuplestore state");
			break;
	}
}

/*
 * tuplestore_copy_read_pointer - 将一个读指针的状态复制到另一个
 */
void tuplestore_copy_read_pointer(Tuplestorestate *fc_state,
							 int fc_srcptr, int fc_destptr)
{
	TSReadPointer *fc_sptr = &fc_state->readptrs[fc_srcptr];
	TSReadPointer *fc_dptr = &fc_state->readptrs[fc_destptr];

	Assert(fc_srcptr >= 0 && fc_srcptr < fc_state->readptrcount);
	Assert(fc_destptr >= 0 && fc_destptr < fc_state->readptrcount);

	/* 分配给自身是无操作 */
	if (fc_srcptr == fc_destptr)
		return;

	if (fc_dptr->eflags != fc_sptr->eflags)
	{
		/* 可能的整体 eflags 变化，因此复制后重新计算 */
		int			fc_eflags;
		int			fc_i;

		*fc_dptr = *fc_sptr;
		fc_eflags = fc_state->readptrs[0].eflags;
		for (fc_i = 1; fc_i < fc_state->readptrcount; fc_i++)
			fc_eflags |= fc_state->readptrs[fc_i].eflags;
		fc_state->eflags = fc_eflags;
	}
	else
		*fc_dptr = *fc_sptr;

	switch (fc_state->status)
	{
		case TSS_INMEM:
		case TSS_WRITEFILE:
			/* 无工作 */
			break;
		case TSS_READFILE:

			/*
			 * 这个情况有点棘手，因为活动读指针的
			 * 位置对应于查找点，而不是其
			 * 变量中的内容。将活动指针分配需要寻址，而
			 * 从活动指针分配需要告知，除了在
			 * eof_reached 的情况下。
			 */
			if (fc_destptr == fc_state->activeptr)
			{
				if (fc_dptr->eof_reached)
				{
					if (BufFileSeek(fc_state->myfile,
									fc_state->writepos_file,
									fc_state->writepos_offset,
									SEEK_SET) != 0)
						ereport(ERROR,
								(errcode_for_file_access(),
								 errmsg("could not seek in tuplestore temporary file")));
				}
				else
				{
					if (BufFileSeek(fc_state->myfile,
									fc_dptr->file, fc_dptr->offset,
									SEEK_SET) != 0)
						ereport(ERROR,
								(errcode_for_file_access(),
								 errmsg("could not seek in tuplestore temporary file")));
				}
			}
			else if (fc_srcptr == fc_state->activeptr)
			{
				if (!fc_dptr->eof_reached)
					BufFileTell(fc_state->myfile,
								&fc_dptr->file,
								&fc_dptr->offset);
			}
			break;
		default:
			elog(ERROR, "invalid tuplestore state");
			break;
	}
}

/*
 * tuplestore_trim - 移除所有不再需要的元组
 *
 * 调用此函数授权 tuplestore 删除所有在最旧读指针之前的元组，
 * 如果没有读指针被标记为需要 REWIND 能力。
 *
 * 注意：如果没有指针具有 BACKWARD 能力，这显然是安全的。
 * 如果一个指针被标记为 BACKWARD 但不具备 REWIND 能力，
 * 这意味着该指针可以向后移动，但不能超过最旧的其他读
 * 指针之前。
 */
void tuplestore_trim(Tuplestorestate *fc_state)
{
	int			fc_oldest;
	int			fc_nremove;
	int			fc_i;

	/*
	 * 如果任何读指针需要重绕能力，则禁止截断。
	 */
	if (fc_state->eflags & EXEC_FLAG_REWIND)
		return;

	/*
	 * 我们不打算修剪临时文件，因为这通常意味着比让它们
	 * 坐在内核缓冲区直到过期更多的工作。
	 */
	if (fc_state->status != TSS_INMEM)
		return;

	/* 找到最旧的读指针 */
	fc_oldest = fc_state->memtupcount;
	for (fc_i = 0; fc_i < fc_state->readptrcount; fc_i++)
	{
		if (!fc_state->readptrs[fc_i].eof_reached)
			fc_oldest = Min(fc_oldest, fc_state->readptrs[fc_i].current);
	}

	/*
	 * 注意：你可能会认为我们可以移除所有最旧
	 * “当前”之前的元组，因为这是下一个将被返回的元组。
	 * 然而，由于 tuplestore_gettuple 返回一个指向我们内部副本的直接指针，
	 * 调用者很可能仍然在一个槽中引用 “当前” 之前的元组。
	 * 所以我们在最旧的 “当前” 之前保留一个额外的元组。
	 * （严格来说，我们可以要求此类调用者使用 “copy” 标志
	 * 来 tuplestore_gettupleslot，但为了效率，我们允许这个特殊
	 * 情况不使用 “copy”。）
	 */
	fc_nremove = fc_oldest - 1;
	if (fc_nremove <= 0)
		return;					/* 无需操作 */

	Assert(fc_nremove >= fc_state->memtupdeleted);
	Assert(fc_nremove <= fc_state->memtupcount);

	/* 释放不再需要的元组 */
	for (fc_i = fc_state->memtupdeleted; fc_i < fc_nremove; fc_i++)
	{
		FREEMEM(fc_state, GetMemoryChunkSpace(fc_state->memtuples[fc_i]));
		pfree(fc_state->memtuples[fc_i]);
		fc_state->memtuples[fc_i] = NULL;
	}
	fc_state->memtupdeleted = fc_nremove;

	/* 标记 tuplestore 为已截断（仅用于 Assert 交叉检查） */
	fc_state->truncated = true;

	/*
	 * 如果 nremove 小于 1/8 的 memtupcount，就在这里停止，留下
	 * “已删除” 的槽为 NULL。这防止我们反复进行 O(N^2) 时间
	 * 的大指针数组的 memmove。最坏情况下的空间
	 * 浪费相当小，因为仅是指针而不是整个元组。
	 */
	if (fc_nremove < fc_state->memtupcount / 8)
		return;

	/*
	 * 将数组向下滑动并重新调整指针。
	 *
	 * 在mergejoin的当前使用中，可以证明始终会有
	 * 恰好一个未删除的元组；因此优化这个情况。
	 */
	if (fc_nremove + 1 == fc_state->memtupcount)
		fc_state->memtuples[0] = fc_state->memtuples[fc_nremove];
	else
		memmove(fc_state->memtuples, fc_state->memtuples + fc_nremove,
				(fc_state->memtupcount - fc_nremove) * sizeof(void *));

	fc_state->memtupdeleted = 0;
	fc_state->memtupcount -= fc_nremove;
	for (fc_i = 0; fc_i < fc_state->readptrcount; fc_i++)
	{
		if (!fc_state->readptrs[fc_i].eof_reached)
			fc_state->readptrs[fc_i].current -= fc_nremove;
	}
}

/*
 * tuplestore_in_memory
 *
 * 如果tuplestore没有溢出到磁盘则返回true。
 *
 * XXX 暴露这个是违反模块化的... 应该去掉它。
 */
bool tuplestore_in_memory(Tuplestorestate *fc_state)
{
	return (fc_state->status == TSS_INMEM);
}


/*
 * 磁带接口例程
 */

static unsigned int fc_getlen(Tuplestorestate *fc_state, bool fc_eofOK)
{
	unsigned int fc_len;
	size_t		fc_nbytes;

	fc_nbytes = BufFileRead(fc_state->myfile, (void *) &fc_len, sizeof(fc_len));
	if (fc_nbytes == sizeof(fc_len))
		return fc_len;
	if (fc_nbytes != 0 || !fc_eofOK)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not read from tuplestore temporary file: read only %zu of %zu bytes",
						fc_nbytes, sizeof(fc_len))));
	return 0;
}


/*
 * 针对HeapTuple情况的专用例程
 *
 * 存储的形式实际上是MinimalTuple，但出于历史原因
 * 我们允许COPYTUP从HeapTuple工作。
 *
 * 由于MinimalTuple的第一个词已经包含长度，我们不需要
 * 单独写那个。
 */

static void * fc_copytup_heap(Tuplestorestate *fc_state, void *fc_tup)
{
	MinimalTuple fc_tuple;

	fc_tuple = minimal_tuple_from_heap_tuple((HeapTuple) fc_tup);
	USEMEM(fc_state, GetMemoryChunkSpace(fc_tuple));
	return (void *) fc_tuple;
}

static void fc_writetup_heap(Tuplestorestate *fc_state, void *fc_tup)
{
	MinimalTuple fc_tuple = (MinimalTuple) fc_tup;

	/* 我们将要写入的MinimalTuple部分： */
	char	   *fc_tupbody = (char *) fc_tuple + MINIMAL_TUPLE_DATA_OFFSET;
	unsigned int fc_tupbodylen = fc_tuple->t_len - MINIMAL_TUPLE_DATA_OFFSET;

	/* 磁盘上的总占用空间： */
	unsigned int fc_tuplen = fc_tupbodylen + sizeof(int);

	BufFileWrite(fc_state->myfile, (void *) &fc_tuplen, sizeof(fc_tuplen));
	BufFileWrite(fc_state->myfile, (void *) fc_tupbody, fc_tupbodylen);
	if (fc_state->backward)		/* 需要尾部长度字吗？ */
		BufFileWrite(fc_state->myfile, (void *) &fc_tuplen, sizeof(fc_tuplen));

	FREEMEM(fc_state, GetMemoryChunkSpace(fc_tuple));
	heap_free_minimal_tuple(fc_tuple);
}

static void * fc_readtup_heap(Tuplestorestate *fc_state, unsigned int fc_len)
{
	unsigned int fc_tupbodylen = fc_len - sizeof(int);
	unsigned int fc_tuplen = fc_tupbodylen + MINIMAL_TUPLE_DATA_OFFSET;
	MinimalTuple fc_tuple = (MinimalTuple) palloc(fc_tuplen);
	char	   *fc_tupbody = (char *) fc_tuple + MINIMAL_TUPLE_DATA_OFFSET;
	size_t		fc_nread;

	USEMEM(fc_state, GetMemoryChunkSpace(fc_tuple));
	/* 读取元组主体 */
	fc_tuple->t_len = fc_tuplen;
	fc_nread = BufFileRead(fc_state->myfile, (void *) fc_tupbody, fc_tupbodylen);
	if (fc_nread != (size_t) fc_tupbodylen)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not read from tuplestore temporary file: read only %zu of %zu bytes",
						fc_nread, (size_t) fc_tupbodylen)));
	if (fc_state->backward)		/* 需要尾部长度字吗？ */
	{
		fc_nread = BufFileRead(fc_state->myfile, (void *) &fc_tuplen, sizeof(fc_tuplen));
		if (fc_nread != sizeof(fc_tuplen))
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not read from tuplestore temporary file: read only %zu of %zu bytes",
							fc_nread, sizeof(fc_tuplen))));
	}
	return (void *) fc_tuple;
}
